﻿/* 
 * Copyright (c) 2011-12 Andrey Bulygin
 * 
 * This file is part of Interceptor.
 * Interceptor is free software: you can redistribute it and/or modify it under the terms of the GNU 
 * Library General Public License as published by the Free Software Foundation, either version 3 of the 
 * License, or (at your option) any later version.
 * 
 * Interceptor is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 
 * the GNU Library General Public License for more details. You should have received a copy of the GNU 
 * Library General Public License along with Interceptor. If not, write to the Free Software Foundation, Inc., 59 
 * Temple Place, Suite 330, Boston, MA 02111-1307 USA.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Emit;
using System.Reflection;
using Interceptor.Core.Objects;
using System.Linq.Expressions;

namespace Interceptor.Core
{
	internal static class InvokerFactory
	{
		const string DEFAULT_INVOKER_NAME = "Interceptor.AutoGeneratedClasses.Invokers.<>Invoker";

		public static Type GenerateInvoker(ModuleBuilder moduleBuilder, MethodsMapping methodsMapping, Type delegateType, List<LambdaExpression> prepareActions, bool callVirt = true)
		{
			return KeyValueCache<Pair<MethodInfo, MethodInfo>, Type>.TryToGet(
				new Pair<MethodInfo, MethodInfo>(methodsMapping.InterceptorMethod.Method, methodsMapping.TargetMethod),
				(p) => GenerateDelegateInvoker(moduleBuilder, methodsMapping, delegateType, prepareActions, callVirt));
		}

		public static Type GenerateDelegateInvoker(ModuleBuilder moduleBuilder, MethodsMapping methodsMapping, Type delegateType, List<LambdaExpression> prepareActions, bool callVirt = true)
		{
			var invoker = moduleBuilder.DefineType(DEFAULT_INVOKER_NAME + Helpers.GenerateNewUniqueIdentiricator(),
													TypeAttributes.Public | TypeAttributes.AutoClass |
													TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit,
													typeof(Invoker<>).MakeGenericType(delegateType));
			var parameterIndex = 0;
			var itcGenericArguments = methodsMapping.TargetMethod.GetGenericArguments();
			var typeGenericParametersNames = itcGenericArguments.Select(x => "T" + ++parameterIndex).ToArray();
			var invokerGenericParameters = typeGenericParametersNames.Length > 0 ? invoker.DefineGenericParameters(typeGenericParametersNames)
																				: new GenericTypeParameterBuilder[0];

			var genericArgumentParameter = new Dictionary<Type, Type>();
			for (int i = 0; i < typeGenericParametersNames.Length; i++)
			{
				genericArgumentParameter.Add(itcGenericArguments[i], invokerGenericParameters[i]);
				invokerGenericParameters[i].SetInterfaceConstraints(itcGenericArguments[i].GetInterfaces());
				if (itcGenericArguments[i].BaseType != typeof(object))
					invokerGenericParameters[i].SetBaseTypeConstraint(itcGenericArguments[i].BaseType);
			}
			Func<Type, Type> getProperTypeForInvoker = (t) => genericArgumentParameter.ContainsKey(t) ? genericArgumentParameter[t] : t;
			//===== PRIVATE ===
			var targetFld = invoker.DefineField("_source", methodsMapping.TargetMethod.DeclaringType, FieldAttributes.Private);

			var paramsFld = methodsMapping.ParametersMapping.Where(x => x.IsLinkedItcParameter && x.ItcParameter.IsBatch)
															.Select(x => x.TargetParameter)
															.Select(x => 
																		invoker.DefineField(
																					x.Name
																					, getProperTypeForInvoker(x.ParameterType.IsByRef
																																? x.ParameterType.GetElementType()
																																: x.ParameterType)
																					, FieldAttributes.Public)).ToArray();
			//===== CTOR ======
			var ctor = invoker.DefineConstructor(MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.RTSpecialName | MethodAttributes.SpecialName, CallingConventions.Standard,
										new Type[] { targetFld.FieldType }
										.Concat(paramsFld.Select(x => x.FieldType))
										.ToArray());

			var ctrIL = ctor.GetILGenerator();
			ctrIL.Emit(OpCodes.Ldarg_0);
			ctrIL.Emit(OpCodes.Call, typeof(Invoker<>).MakeGenericType(delegateType).GetConstructor(Type.EmptyTypes));
			ctrIL.Emit(OpCodes.Ldarg_0);
			ctrIL.Emit(OpCodes.Ldarg_1);
			ctrIL.Emit(OpCodes.Stfld, targetFld);
			for (int i = 0; i < paramsFld.Length; i++)
			{
				ctrIL.Emit(OpCodes.Ldarg_0);
				ctrIL.EmitLdarg((short)(i + 2));
				ctrIL.Emit(OpCodes.Stfld, paramsFld[i]);
			}

			ctrIL.Emit(OpCodes.Ret);
			//===== END CTOR ==
			//===== DEFINE INVOKER
			var defineInvokerMethod = invoker.DefineMethod("DefineInvokerMethod", MethodAttributes.Public | MethodAttributes.HideBySig, typeof(void), new[] { delegateType });
			var invMethIL = defineInvokerMethod.GetILGenerator();
			invMethIL.Emit(OpCodes.Ldarg_0);
			invMethIL.Emit(OpCodes.Ldarg_1);
			invMethIL.Emit(OpCodes.Call, typeof(Invoker<>).MakeGenericType(delegateType).GetMethod("set_Invoke", BindingFlags.NonPublic | BindingFlags.Instance));
			invMethIL.Emit(OpCodes.Ret);
			//=================
			//===== INVOKER ===
			var definedTargetMethod = methodsMapping.TargetMethod.IsGenericMethod ? methodsMapping.TargetMethod.MakeGenericMethod(invokerGenericParameters)
																		  : methodsMapping.TargetMethod;

			var targetReturnType = getProperTypeForInvoker(definedTargetMethod.ReturnType);
			targetReturnType = targetReturnType != typeof(void) ? targetReturnType : typeof(VoidResult);
			Type invokerMethodReturnType = !methodsMapping.InterceptorMethod.ReturnType.IsGenericParameter ? methodsMapping.InterceptorMethod.ReturnType
																										   : targetReturnType;
			Type[] invokerMethodParametersTypes =
				methodsMapping.InterceptorMethod.Parameters.Where(x => !x.IsBatch)
															.Select(x => (!x.ParameterInfo.ParameterType.IsGenericParameter ? x.ParameterInfo.ParameterType
																							: getProperTypeForInvoker(methodsMapping.ParametersMapping.First(m => m.ItcParameter == x).TargetParameter.ParameterType)))
															.ToArray();

			var methodBuilder = invoker.DefineMethod("InvokeMethod", MethodAttributes.Public | MethodAttributes.HideBySig
														, invokerMethodReturnType
														, invokerMethodParametersTypes);

			var methIL = methodBuilder.GetILGenerator();
			methIL.Emit(OpCodes.Ldarg_0);
			methIL.Emit(OpCodes.Ldfld, targetFld);
			int argumentIndex = 0;
			int fieldIndex = 0;
			foreach (var pm in methodsMapping.ParametersMapping)
			{
				if (!pm.ItcParameter.IsBatch)
				{
					methIL.EmitLdarg((short)(argumentIndex + 1));
					if (!invokerMethodParametersTypes[argumentIndex++].IsValueType &&
						pm.TargetParameter.ParameterType.IsValueType)
					{
						methIL.Emit(OpCodes.Unbox_Any, pm.TargetParameter.ParameterType);
					}
				}
				else
				{
					if (pm.IsLinkedItcParameter)
					{
						methIL.Emit(OpCodes.Ldarg_0);

						if (pm.TargetParameter.ParameterType.IsByRef)
							methIL.Emit(OpCodes.Ldflda, paramsFld[fieldIndex++]);
						else
							methIL.Emit(OpCodes.Ldfld, paramsFld[fieldIndex++]);

					}
				}
			}

			if (callVirt)
				methIL.Emit(OpCodes.Callvirt, definedTargetMethod);
			else 
				methIL.Emit(OpCodes.Call, definedTargetMethod);

			if (invokerMethodReturnType != typeof(void) &&
					definedTargetMethod.ReturnType == typeof(void))
				methIL.Emit(OpCodes.Ldnull);
			methIL.Emit(OpCodes.Ret);

			//===== END INVOKER
			var result = invoker.CreateType();

			if (methodsMapping.InterceptorMethod.InterceptorType == ItcType.Invoker)
			{
				var defineMethodInfo = typeof(Invoker<>).MakeGenericType(delegateType).GetMethod("DefineMethodInfo");
				prepareActions.Add(LambdaExpression.Lambda(LambdaExpression.Call(defineMethodInfo, LambdaExpression.Constant(methodsMapping.TargetMethod))));
			}

			return result;

		}
	}
}
